java网络-TCP/IP 三次握手与四次分手-理论

20240804224559

java网络-TCP/IP 三次握手与四次分手-理论

学习,学习一个知识就像扒洋葱,从外向里一层一层的扒。

任何一个知识,尤其是缩写,知道和熟悉它的全拼是很关键的。

通过本文,你可以了解到如下理论知识

  1. 三次握手的简要流程
  2. 三次握手的简要流程中状态和方法的对应关系
  3. 三次握手过程中涉及的系统配置查看和控制
  4. 四次分手的简要流程
  5. 四次分手的简要流程中状态和方法的对应关系
  6. 四次分手过程中涉及的系统配置查看和控制

什么是 TCP

TCP 全拼:Transmission Control Protocol,中文:传输控制协议。它是一种 面向连接的可靠的基于字节流的 传输层 `通信协议

它首先是个协议,其次,它是个控制通信的协议,再次,它是个面向连接的、可靠的、基于字节流的通信协议。

  • TIPS

  • 数据在TCP层称为流(Stream),数据分组称为分段(Segment)。作为比较,数据在IP层称为Datagram,数据分组称为分片(Fragment)。 UDP 中分组称为Message。

TCP协议的运行方式

TCP协议的运行分为三个阶段:连接建立(connection establishment)、数据传送(data transfer)和连接终止(connection termination)。操作系统将TCP连接抽象为套接字 socket,作为编程接口给程序使用。在TCP连接的生命期内,TCP连接要经历一系列的状态改变。参见:TCP状态流转图

三次握手和四次断开算是TCP的核心了,它涉及怎么握手,握手用到哪些资源,怎么控制握手,怎么控制资源,怎么断开等等

TCP建立连接 - 三次握手

握手的目的是建立 连接,服务端面对这个 连接 请求,会创建一个套接字 socket 给予回应。

正常的三次握手过程:

  1. 第一次握手:Client 端向 Server 端发送 SYN 报文,Client 端进入 SYN_SENT 状态
  2. 第二次握手:Server 端收到 Client端的 SYN 报文后,内核首先会将 连接 存储到 SYN Queue 半连接队列,然后向 Client 回复 SYN+ACK 报文,然后进入 SYN_RECV 状态
  3. 第三次握手:Client 端收到 Server 端的 SYN+ACK 报文后,Client 端回复 ACK,然后进入 ESTABLISHED 状态。
  4. Server 端收到 Client 端的 ACK 报文后,内核将 连接SYN Queue 半连接队列 中取出,放入 Accept Queue 全连接队列,然后进入 ESTABLISHED 状态

到此,三次握手完成。双方可以发送应用数据了。

在第三次握手前半部分,Client 端收到 SYN+ACK 报文时,Client 端的 connect() 成功返回;三次握手成功完成后,accept() 方法成功返回。此时,Server 端从 Accept Queue 全连接队列 中取出 连接。通过这个 连接 Client 端和 Server 端进行应用数据交互。

20240728131348

三次握手过程中涉及的系统配置查看和控制

正常的三次握手正如以上。那么,不正常的呢?整个过程中任何一步的异常都是不正常的。如:

  1. Client 端发送了,Server 端没有回复
  2. SYN Queue 半连接队列满了
  3. Accept Queue 全连接队列满了

针对这些异常点,系统是如何控制的呢?我们看看系统是如何控制的,如何查看,如何更新

20240728230341

tcp_syn_retries

Client 端发送 SYN 报文给 Server 端,在发送过程中可能会失败,或者由于其他原因 Server 端没有收到。这时,Client 需要重传,即超时重传机制,但又不能一直重试,所以就有了 tcp_syn_retries

详细参考:
截屏2024-07-28-1出处:TCP连接的建立和断开受哪些系统配置影响?

tcp_synack_retries

tcp_syn_retries 一样,Server 端发送给 Client 端的 SYN+ACK 报文,Client 端也可能没有收到,所以也有超时重试机制 tcp_synack_retries

todo 如何更新这个值呢

半连接队列最大长度

这里有个前提:我们讨论的系统环境都是 CentOS 7,同时这里我们基于java 语言

linux 内核版本小于 2.6.20 时,半连接队列最大长度等于 /proc/sys/net/ipv4/tcp_max_syn_backlog;大于2.6.20时,半连接队列最大长度为一系列计算后的结果,如下:

1
2
3
4
5
6
7
8
backlog = min(somaxconn, backlog)
nr_table_entries = backlog
nr_table_entries = min(backlog, sysctl_max_syn_backlog)
nr_table_entries = max(nr_table_entries, 8)
// roundup_pow_of_two: 将参数向上取整到最小的 2^n,注意这里存在一个 +1
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1)
max_qlen_log = max(3, log2(nr_table_entries))
max_queue_length = 2^max_qlen_log

可以看到,半连接队列的最大长度由三个参数指定:

  • 调用 listen 时,传入的 backlog
  • /proc/sys/net/core/somaxconn 默认值为 128
  • /proc/sys/net/ipv4/tcp_max_syn_backlog 默认值为 1024

我们假设 listen 传入的 backlog = 128 (调用 listen 时传递的 backlog 参数使用的是 /proc/sys/net/core/somaxconn),其他配置采用默认值,来计算下半连接队列的最大长度

1
2
3
4
5
6
7
backlog = min(somaxconn, backlog) = min(128, 128) = 128
nr_table_entries = backlog = 128
nr_table_entries = min(backlog, sysctl_max_syn_backlog) = min(128, 1024) = 128
nr_table_entries = max(nr_table_entries, 8) = max(128, 8) = 128
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1) = 256
max_qlen_log = max(3, log2(nr_table_entries)) = max(3, 8) = 8
max_queue_length = 2^max_qlen_log = 2^8 = 256

所以,可以得到半队列最大大小是 256。

修改半连接队列最大长度

我们已经知道了半连接队列的最大长度是如何计算的,受哪些配置影响,如下

  1. 调用 listen 时,传入的 backlog
  2. /proc/sys/net/core/somaxconn 默认值为 128
  3. /proc/sys/net/ipv4/tcp_max_syn_backlog 默认值为 1024

对于 listen 函数,传入的参数由程序编写时控制。

对于/proc/sys/net/core/somaxconn,这个值也影响 全连接队列,参见 全连接队列最大长度 部分。

对于 /proc/sys/net/ipv4/tcp_max_syn_backlog

  • 查看如下

    方法一 $ cat /proc/sys/net/ipv4/tcp_max_syn_backlog

方法二 $ sysctl -a | grep xxx

  • 修改如下

方法一 通过“/proc/sys”目录,使用echo命令修改内核参数对应的文件

1
$ echo "1024" > /proc/sys/net/ipv4/tcp_max_syn_backlog

方法二 通过“/etc/sysctl.conf”文件进行修改

1
2
3
4
5
6
1、更新 /etc/sysctl.conf 文件,新增一行 net.ipv4.tcp_max_syn_backlog
$ vim /etc/sysctl.conf
net.ipv4.tcp_max_syn_backlog=1024

2、执行 sysctl -p 使配置生效
$ sudo sysctl -p
半连接队列满了 - net.ipv4.tcp_syncookies

如前所述,半连接队列长度超过最大长度,怎么办呢?

有一种攻击叫做 SYN Flood:Client 端高频的向 Server 端发送 SYN 报文,让 Server 端的半连接队列满了,队列耗尽。

这时候,net.ipv4.tcp_syncookies=1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

留个作业:SYN Cookie 的原理是什么样的呢?怎么解决的 SYN Flood

全连接队列最大长度

TCP 全连接队列的最大长度由 min(somaxconn, backlog) 控制,其中:

  • somaxconn 是 Linux 内核参数,由 /proc/sys/net/core/somaxconn 指定
  • backlog 是 TCP 协议中 listen 函数的参数之一,即 listen(int sockfd, int backlog) 函数中的 backlog 大小。listen 的 backlog 参数使用的是 /proc/sys/net/core/somaxconn 文件中的值。即 net.core.somaxconn 的值

代码位置:socket.c

  • 查看 somaxconn 的值

    方法一 $ cat /proc/sys/net/core/somaxconn

方法二 $ sysctl -a | grep xxx

  • 修改 somaxconn 的值

    方法一 通过“/proc/sys”目录,使用echo命令修改内核参数对应的文件

1
$ echo "1024" > /proc/sys/net/core/somaxconn

方法二 通过“/etc/sysctl.conf”文件进行修改

1
2
3
4
5
6
1、更新 /etc/sysctl.conf 文件,新增一行 net.core.somaxconn=1024 
$ vim /etc/sysctl.conf
net.core.somaxconn=1024

2、执行 sysctl -p 使配置生效
$ sudo sysctl -p

再查下值是否变化了

$ cat /proc/sys/net/core/somaxconn

全连接队列满了 - net.ipv4.tcp_abort_on_overflow

全连接队列 满了之后,新来的 全连接 将被丢弃。Server 在丢弃新连接时,有时需要发送 reset 给 client,这样 client就不会重试了。但是,默认是不发送 reset的。所以,是否要发送 reset,通过 tcp_abort_on_overflow 来控制,为0则不发送。

不发送有个好处,给了 client 一次重试的机会。

查看半连接队列和全连接队列的当前值和最大值

ss命令

可以通过 ss 命令查看全连接队列的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
# -n 不解析服务名称
# -t 只显示 tcp sockets
# -l 显示正在监听(LISTEN)的 sockets

$ ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 [::]:80 [::]:*
LISTEN 0 128 [::]:8080 [::]:*

$ ss -nt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 536 [::ffff:33.9.95.134]:80 [::ffff:33.51.103.59]:38280
ESTAB 0 0 [::ffff:33.9.95.134]:80

对于 LISTEN 状态的 socket
  • Recv-Q: 表示当前全连接队列的大小,即已完成三次握手等待 accept() 的 TCP 连接
  • Send-Q: 表示全连接队列的最大大小
对于 非LISTEN 状态的 socket
  • Recv-Q: 表示已收到但未被应用程序确认的字节数,即数据已放入 tcp/ip 接收缓冲区,但应用程序不掉用 recv() 将其从 tcp/ip 缓冲区中复制到应用缓冲区。
  • Send-Q: 表示数据已放入 tcp/ip 发送缓冲区,但未发送或已发送但未 ACK的字节数。因此,Send-Q 的值可能与服务器网络拥塞、服务器性能问题或数据包流程控制有关。

系统配置参考

20240728164700
出处:TCP连接的建立和断开受哪些系统配置影响?

终止连接 - 四次断开

三次握手的发起点是 Client 端。但四次分手的发起点却是 Client 端和 Server 端都可以。

首先调用 close() 的一方叫主动关闭,而接收到 FIN 报文再调用 close() 的一方叫被动关闭。

当应用程序调用 close() 时,会向对端发送 FIN 报文,然后收到 ACK 报文。对方也会调用 close(),也会发送 FIN 报文,也会收到 ACK 报文。整个四次分手就是这样的。

我们暂且假设是由 Client 端发起的分手。

20240728234248

如上图,正常的四次分手的过程:

  1. Client 端要关闭连接(调用 close()),此时会发送 FIN 报文,然后进入 FIN_WAIT_1 状态。
  2. Server 端收到 FIN 报文后,会回复 ACK 报文,然后进入 CLOSE_WAIT 状态。
  3. Client 端收到 ACK 报文后,进入 FIN_WAIT_2 状态。
  4. 等待 Server 端程序处理完数据后(调用 close()),向 Client 端发送 FIN 报文,然后进入 LAST_ACK 状态。
  5. Client 端收到 FIN 报文后,回复 ACK 报文,然后进入 TIME_WAIT 状态。
  6. Server 端收到 ACK 报文后,进入 CLOSED 状态,Server 端完成连接关闭。
  7. Client 端在经过 2msl 后,自动进入 CLOSED 状态,Client 端完成连接关闭。

四次分手过程中涉及的系统配置查看和控制

20240804222834

正常的四次分手如以上。那么,不正常的呢?整个过程中任何一步的异常都是不正常的。如:

  1. Client 端发送了,Server 端没有回复
  2. Client 端没有收到 Server 端的 FIN 报文
  3. Server 端没有收到 Client 端的 ACK 报文
  4. 等等

针对这些异常点,系统是如何控制的呢?我们看看系统是如何控制的,如何查看,如何更新

注意:主动关闭的一方才有 FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT 状态,被动关闭的一方才有 CLOST_WAIT 状态

第一次分手丢失了 - net.ipv4.tcp_orphan_retries

如果第一次分手丢失了,那么 Client 端迟迟收不到 Server 端的 ACK。

Client 端发送 FIN 报文后,如果迟迟收不到 Server 端的 ACK 报文,则会触发超时重传机制。重发次数由 tcp_orphan_retries 控制。如果超过这个次数仍没有成功,则 Client 端直接进入 CLOSED 状态。断开连接,连接关闭。

  • 查看 net.ipv4.tcp_orphan_retries 的值

    方法一 $ cat /proc/sys/net/ipv4/tcp_orphan_retries

方法二 $ sysctl -a |grep orphan_retries

  • 修改 net.ipv4.tcp_orphan_retries 的值

    方法一 通过“/proc/sys”目录,使用echo命令修改内核参数对应的文件

    1
    echo "0" > /proc/sys/net/ipv4/tcp_orphan_retries

方法二 通过 /etc/sysctl.conf 文件进行修改

1
2
3
4
5
6
1、更新 /etc/sysctl.conf 文件,新增一行 net.ipv4.tcp_orphan_retries=3
$ vim /etc/sysctl.conf
net.ipv4.tcp_orphan_retries=3

2、执行 sysctl -p 使配置生效
$ sudo sysctl -p

使用 echo value 方式直接追加到文件中。 如 echo "1" > /proc/sys/net/ipv4/tcp_syn_retries ,但是这种方式设备重启后,会恢复成默认值。
把参数添加到 vim /etc/sysctl.conf 中,然后执行 sysctl -p 使参数生效。这种方式是永久有效的。

第二次分手丢失了 - net.ipv4.tcp_orphan_retries

第一次没丢,意味着 FIN 被 Server 端接收到,Server 端发送给 Client 端的 ACK 丢失了,而 ACK 报文是不会重传的,所以,Client 端就会触发超时重传机制,重传 FIN 报文。

第二次分手丢失了,其结果也是 Client 端迟迟收不到 Server 端的 ACK。

第三次分手丢了 - Server 端超时重传机制 - net.ipv4.tcp_orphan_retries

当Server 端应用程序在处理完数据后,调用 close() 函数,执行第三次分手:进入 LAST_ACK 状态,然后发送 FIN 报文,等待着 Client 端回复 ACK 报文。

如果 Server 端迟迟没有收到 Client 端的 ACK 报文,Server 端会重发 FIN 报文,重发次数还是由 tcp_orphan_retires 控制。

超过重试次数后,Server 端断开连接,连接关闭。

第三次分手丢失了 - 客户端超时关闭 - net.ipv4.tcp_fin_timeout

Client 端收到 Server 端的 ACK 报文后,进入 FIN_WAIT_2 状态,这时,Client 端开始等待着 Server 端的 FIN 报文,如果迟迟没有收到 FIN 报文,则会触发超时机制,默认是60s。

超过超时时间,Client 端断开连接,连接关闭。

这里注意:对于调用close() 函数关闭的连接,如果在60秒后还没有收到 FIN 报文,Client 端(主动关闭方)的连接就会直接关闭;但如果 Client 端(主动关闭方)调用 shutdown() 函数关闭连接,shutdown() 是优雅关闭,它是只关闭发送方向,而接收方向还是可以接受数据的。所以,这种情况下,如果 Server 端一直没有收到 FIN 报文,Client 端的状态会一直是 FIN_WAIT_2。(注:net.ipv4.tcp_fin_timeout 无法控制 shutdown() 关闭的连接)

  • 查看和修改 net.ipv4.tcp_fin_timeout 值的方法同以上 net.ipv4.tcp_orphan_retries
第四次分手丢失了 - net.ipv4.tcp_orphan_retries

Client 发送了第四次分手的报文 ACK,Server 端如果没有收到,会重发第三次分手的报文 FIN。重发控制还是 tcp_orphan_retries。

Client - TIME_WAIT 状态控制

Client 端发送了第四次分手的 ACK 报文后,进入了 TIME_WAIT 状态。这个状态在 2msl时间周期后,会自动变成 CLOSED 状态。

对于 TIME_WAIT 时间周期的控制是由系统参数 net.ipv4.tcp_max_tw_buckets 控制的。默认是60s。

对于 TIME_WAIT 状态还有一个控制:是否可以 复用 处于 TIME_WAIT 状态的 tcp 连接。

复用 的原因:Client 关闭跟 Server 的连接后,也有可能很快再次跟 Server 之间建立一个新的连接,而由于 TCP 端口最多只有 65536 个,如果不去复用处于 TIME_WAIT 状态的连接,就可能在快速重启应用程序时,出现端口被占用而无法创建新连接的情况

是否可以 复用 的是由系统参数 net.ipv4.tcp_tw_reuse 控制的。1表示能复用,0表示不能服用。

  • 查看 net.ipv4.tcp_max_tw_buckets 的值

    方法一 $ cat /proc/sys/net/ipv4/tcp_max_tw_buckets

方法二 $ sysctl -a |grep tw_buckets

  • 修改 net.ipv4.tcp_max_tw_buckets 的值

    方法一 通过“/proc/sys”目录,使用echo命令修改内核参数对应的文件

    1
    echo "0" > /proc/sys/net/ipv4/tcp_max_tw_buckets

方法二 通过“/etc/sysctl.conf”文件进行修改

1
2
3
4
5
6
1、更新 /etc/sysctl.conf 文件,新增一行 net.ipv4.tcp_max_tw_buckets=30
$ vim /etc/sysctl.conf
net.ipv4.tcp_max_tw_buckets=30

2、执行 sysctl -p 使配置生效
$ sudo sysctl -p

  • 查看 net.ipv4.tcp_tw_reuse 的值

    方法一 $ cat /proc/sys/net/ipv4/tcp_tw_reuse

方法二 $ sysctl -a | grep tw_reuse

  • 修改 net.ipv4.tcp_tw_reuse 的值

    方法一 二 同 net.ipv4.tcp_max_tw_buckets 的修改方法

通过以上可知,三次握手和四次分手的系统配置信息可通过 $ cat /etc/sysctl.conf 或者 $ sysctl -a 或者 $ cat /proc/sys/net/ipv4/xxx 查看

以上就是 tcp/ip 三次握手和四次分手的理论知识。

理论的知识需要实践的支撑。而且理论偏抽象,实践更具体,感受和认知更深刻。所以,下一篇通过实践验证和支撑以上的理论。

辅助

解决过程中用到的资料

实战代码:bjmashbing-sysio

TCP半连接全连接(一) – 全连接队列相关过程

从一次线上问题说起,详解 TCP 半连接队列、全连接队列

tcp-连接建立

TCP连接的建立和断开受哪些系统配置影响?

tcp-3-way-handshake-process